/**
 * 
 */
package edu.caltech.android.sensor.accel;

import java.util.ArrayList;
import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentLinkedQueue;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcelable;
import android.util.Log;

/**
 * Accelerometer listening is implemented as a "local service", not accessible
 * outside this application. Thus, it should be started via
 * Context.startService() and stopped via Context.stopService() or stopSelf().
 * The service uses intents in a publish-subscribe pattern.
 * 
 * An alternative is to have other activities / services bind to this service,
 * and probably also set up some callback scheme. That might give less overhead?
 * However, lifecycles might get tricky if this service is both started via
 * startService, and bound to.
 * 
 * Older notes:
 * 
 * might be good to implement some of the functionality of:
 * http://blog.androgames.net/85/android-accelerometer-tutorial/ Particularly, I
 * like the isSuported method.
 * 
 * One issue is the variable-rate sampling: if an insufficient number of samples
 * are recorded in a time window ( a tolerance here needs to be specified ),
 * this should be noted somehow.
 * 
 * 
 * @author mfaulk
 * 
 */
public class Accelerometer extends Service {

	// for logging, etc.
	private static final String TAG = "Accelerometer";

	// broadcast intent action name
	public static final String UI_ACCEL_SAMPLES = "edu.caltech.android.sensors.UI_ACCEL_SAMPLES";
	// bundle key
	public static final String UI_ACCEL_BUNDLE = "accelSamples";

	// broadcast intent action name
	public static final String WINDOW_ACCEL_SAMPLES = "edu.caltech.android.sensors.WINDOW_ACCEL_SAMPLES";
	// bundle key
	public static final String WINDOW_ACCEL_BUNDLE = "accelSamples";

	// broadcast intent action name
	public static final String ACCEL_SAMPLE = "edu.caltech.android.sensors.ACCEL_SAMPLE";
	// extras key
	public static final String ACCEL_SAMPLE_KEY = "accelSample";

	private SensorManager sensorManager;
	private Timer broadcastTimer;

	// if the number of samples in a time window is not within this tolerance,
	// it is considered "out of tolerance" and we should do something other than
	// broadcasting as normal.
	// private final double samplingTolerance = 0.10;

	// TODO: shouldn't keep redundant copies of acceleration samples.
	private Queue<AccelSample> uiUpdateSamples;
	private final int uiSampleBound = 50; // was 100, which worked fine

	@Override
	public void onCreate() {
		super.onCreate();
		Log.d(TAG, "creating Accelerometer");

		uiUpdateSamples = new ConcurrentLinkedQueue<AccelSample>();

		// ---- register to listen for accelerometer events ----
		sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
		Sensor accelerometer = sensorManager
				.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
		sensorManager.registerListener(sensorEventListener, accelerometer,
				SensorManager.SENSOR_DELAY_FASTEST);

		// TODO: check for possible concurrency issues with this timer
		broadcastTimer = new Timer("accelGuiUpdateTimer");

		// ---- schedule broadcast for GUI updates ----
		final int guiUpdateMillis = 100;
		broadcastTimer.scheduleAtFixedRate(new TimerTask() {
			public void run() {
				broadcastGuiUpdate();
			}
		}, 0, guiUpdateMillis);
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		sensorManager.unregisterListener(this.sensorEventListener);
		broadcastTimer.cancel();
	}

	/**
	 * Look here for !!! failed binder transaction !!! errors, which can be
	 * caused by putting too many bytes into an intent's extras. This approach
	 * may not be appropriate for sending many samples.
	 *
	 * Broadcast a few (e.g. 5 or 10) samples, for updating the waveform GUI
	 */
	protected void broadcastGuiUpdate() {
		AccelSample[] samples = uiUpdateSamples.toArray(new AccelSample[0]);
		uiUpdateSamples.clear();
		ArrayList<AccelSample> temp = new ArrayList<AccelSample>(samples.length);
		for (AccelSample s : samples) {
			if (s != null) {
				temp.add(s);
			}
		}
		// TODO should only send this if there were some samples?
		
		Bundle bundle = new Bundle();
		bundle.putParcelableArrayList(UI_ACCEL_BUNDLE, temp);
		//
		Intent intent = new Intent(UI_ACCEL_SAMPLES);
		intent.putExtra("accelSamples", bundle);
		sendBroadcast(intent);
	}

	/**
	 * Broadcast a single sample, for use with the initial GMPicker
	 * 
	 * @param sample
	 */
	protected void broadcastSingleSample(AccelSample sample) {
		Intent intent = new Intent(ACCEL_SAMPLE);
		// this cast is to remove the ambiguity of which putExtra method should
		// be called, since AccelSample is both parcelable and serializable
		intent.putExtra(ACCEL_SAMPLE_KEY, (Parcelable) sample);
		sendBroadcast(intent);
	}

	private final SensorEventListener sensorEventListener = new SensorEventListener() {
		// double calibration = SensorManager.STANDARD_GRAVITY;

		public void onAccuracyChanged(Sensor sensor, int accuracy) {
			// do nothing
		}

		public void onSensorChanged(SensorEvent event) {
			float x = event.values[0]; // m/s^2
			float y = event.values[1]; // m/s^2
			float z = event.values[2]; // m/s^2
			// BEWARE! the event timestamp is nanoseconds since the device turned on, not some standard global time!
			//long t = event.timestamp; // nanoseconds
			long t = System.currentTimeMillis();

			AccelSample accelSample = new AccelSample(x, y, z, t);

			// add new sample and trim collections if needed:
			uiUpdateSamples.offer(accelSample);
			trimQueue(uiUpdateSamples, uiSampleBound);

			broadcastSingleSample(accelSample);
		}
	};

	private <T> void trimQueue(Queue<T> queue, int maxLength) {
		if (queue.size() <= maxLength) {
			return;
		}

		while (queue.size() > 0 && queue.size() > maxLength) {
			queue.remove(0);
		}
	}

	@Override
	public IBinder onBind(Intent intent) {
		Log.d(TAG, "onBind");
		return null;
	}

}
